#include "log.h"
#include "net.h"
#include "dial.h"

struct net_packet {
    struct iphdr iph;
    struct icmp icp;
};

static uint32_t ping_rcv(int fd, int bridge_mode, double* diff_time, int icmpId, ping_detail_t *detail)
{
    struct net_packet pkt;
    struct sockaddr_in addr;
    struct icmp *icp, Icp;
    int ret, addr_len;
    struct timeval now;
    uint32_t* snd_time;
    uint32_t tm;

    addr_len = sizeof(addr);
    if (bridge_mode) {
        ret = read(fd, &Icp, sizeof(Icp));
        icp = &Icp;
    } else {
        ret = recvfrom(fd, &pkt, sizeof(pkt), 0, (struct sockaddr*)&addr, &addr_len);
        if(ret > 0){
            // log_trace("ping rcv from addr :%s", inet_ntoa(addr.sin_addr));
        }
        if(pkt.iph.protocol != IPPROTO_ICMP){
            return -1;
        }
        icp = &pkt.icp;
    }
    gettimeofday(&now, NULL);
    // log_buf(LOG_TRACE, &pkt, ret);

    if (ret < 0) {
        log_syserr("ping rcv error.");
        return -1;
    }

    if (icp->icmp_id != icmpId)
        return -1;

    switch (icp->icmp_type) {
    case ICMP_DEST_UNREACH:
        log_warn("ICMP respone unreach");
        log_trace("rcv icmp type:%d, seq:%d", icp->icmp_type, icp->icmp_seq);
        break;
    case ICMP_ECHOREPLY:
        snd_time = (uint32_t*)&icp->icmp_dun;
        tm = (now.tv_sec * 1000000ULL + now.tv_usec - *snd_time);
        *diff_time = (double)tm / 1000.0;
        if(detail){
            detail->latency = *diff_time;
            detail->ttl = pkt.iph.ttl;
        }
        log_trace("ping id:%d, seq:%d, rcv type:%d, diff time:%.3f ms", icp->icmp_id, icp->icmp_seq, icp->icmp_type, *diff_time);
        break;
    default:
        break;
    }
    return icp->icmp_type;
}

void* do_ping(void* _arg)
{
    int fd = -1, ret, seq = 1, resp_seq = 0, snd_count = 0, rcv_count = 0;
    int blocksize = 0;
    double *platencies = NULL, *platency = NULL;
    double rcv_ret = 0, diff_time = 0;
    struct sockaddr_in peer_addr;
    struct icmp* icp;
    fd_set rdfds, wrfds;
    struct timeval tmo = { 0, 0 }, now = { 0, 0 }, send_tm = { 0, 0 };
    char buf[0xffff] = { 0 };
    conn_track ct = { 0 };
    time_t r_now, start_tm;
    dial_arg_t* dial_arg = (dial_arg_t*)_arg;
    dial_result_t* dial_result = calloc(1, sizeof(dial_result_t));
    ping_arg_t* arg = &dial_arg->in.ping_arg;
    ping_result_t* result = &dial_result->out.ping_result;
    ping_detail_t *detail = arg->detail;

    if (arg->ip == 0) {
        log_error("do_ping ipaddr is zero.");
        dial_result->err = PING_ERR_IP_NONE;
        goto out;
    }

    blocksize = arg->blocksize ? arg->blocksize : sizeof(struct icmp);

    if (dial_arg->bridge_mode) {
        ct.dest_ip = arg->ip;
        ct.icmpId = ICMP_ID;
        ct.proto = IPPROTO_ICMP;
        fd = conn_track_regist(&ct);
    } else {

        peer_addr.sin_family = AF_INET;
        peer_addr.sin_addr.s_addr = arg->ip;

        if ((fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)) < 0) {
            log_syserr("Ping socket failed.");
            dial_result->err = PING_ERR_CREAT_SOCK;
            goto out;
        }
    }
    sock_rcvtmo(fd, arg->timeout, 1);
    icp = (struct icmp*)buf;
    fill_icmp_packet(icp, ICMP_ECHO, seq);

    platency = platencies = malloc(sizeof(double) * arg->counts);
    while (1) {
        if (snd_count >= arg->counts && arg->counts)
            break;

        //send
        icmp_set_seq(icp, seq++);
        if (dial_arg->bridge_mode) {
            write(fd, icp, blocksize);
        } else {
            if(0 > sendto(fd, icp, blocksize, 0, (struct sockaddr*)&peer_addr, sizeof(peer_addr))){
                dial_result->err = PING_ERR_SEND;
                goto out;
            }
        }
        snd_count++;

        //rcv
        start_tm = time(NULL);
        while (1) {
            if (ping_rcv(fd, dial_arg->bridge_mode, &rcv_ret, icp->icmp_id, detail) == ICMP_ECHOREPLY) {
                diff_time += rcv_ret;
                *platency = rcv_ret;
                platency++;
                if (result->min_latency == 0)
                    result->min_latency = rcv_ret;
                else
                    result->min_latency = MIN_VAL(result->min_latency, rcv_ret);
                result->max_latency = MAX_VAL(result->max_latency, rcv_ret);
                rcv_count++;
                break;
            } else {
                r_now = time(NULL);
                if(r_now - start_tm > 3){
                    break;
                }
            }
        }
        if(detail){
            detail->blksize = blocksize;
            detail++;
        }
    }
    if (rcv_count == 0) {
        dial_result->err = PING_ERR_NOT_REACH;
    }
    result->latency = rcv_count ? diff_time / rcv_count : 0;
    result->snd_count = snd_count;
    result->rcv_count = rcv_count;
    result->lost_percent = (1.0 - (double)rcv_count / snd_count) * 100;
    result->jitter = cal_jitter(platencies, rcv_count);

out:
    free(platencies);
    if(fd > 0){
        close(fd);
    }
    return (void*)dial_result;
}

void ping_result_print(ping_result_t* result)
{
    log_trace("latency:%.2f ms", result->latency);
    log_trace("max_latency:%.2f ms, min_latency:%.2f ms", result->max_latency, result->min_latency);
    log_trace("snd_count:%d", result->snd_count);
    log_trace("rcv_count:%d", result->rcv_count);
    log_trace("lost_percent:%.2f", result->lost_percent);
    log_trace("jitter:%.2f ms", result->jitter);
}
